
# Copyright (c) 2014, Franklin Brauning V.  All rights reserved.
# 
#    El desarrollo inicial de esta utilidad fue producido
#    por Franklin Brauning <brauningbar@gmail.com>
# 
#    Si consideras que este codigo puede serte util, eres libre
#    de usarlo a conveniencia, pero no olvides mantener la licencia
#    y dar credito al autor.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
#   * Redistributions of source code must retain the above copyright notice, this
#     list of conditions and the following disclaimer.
# 
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions and the following disclaimer in the documentation
#     and/or other materials provided with the distribution.
# 
#   * Neither the name of the author nor the names of its contributors may
#     be used to endorse or promote products derived from this software without
#     specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from django.db import models
from .base import *
import pickle


class TaggedException(AbstractException):
	''' exception for tagged models '''
	pass

cachekey = '_tagcache'

def tagcached(func):
	'''decorator to cache expensive method calls persistently using
	tagged class capabilities.

	Note that this utility memoizes based on function arguments,
	but these must be pickleable too (format 0).

	If there is a need to skip the cache invoke the wrapped method
	with cached=False.

	Cached data will be cleared when due (namely after .save),
	but there arent clever checks to detect invalid/obsolete values
	(not yet?).  Considering this, you should use this only if the
	computed value is supposed to never change.
	'''
	def w(self, *a, cached=True, fallbacktdcache=True, **kw):
		# fallback to memory cache if current register is unsafe to save
		#
		if hasattr(self, '_unsafe') and fallbacktdcache:
			self.microtrace("unsafe register, fallback to tdcache")
			return tdcached()(func)(self, *a, cached=cached, **kw)

		# serializable key because tagged model
		# is saving in format 0 (cant serialize functions, i think)
		#
		k = [cachekey, 'memo', func.__name__, a, tuple(kw.items())]
		if cached:
			x = self.xgettag(*k)
			if x:
				self.microtrace('hit (tagcached)', k)
				return x

		x = func(self, *a, **kw)
		k.append(x)
		if not hasattr(self, '_unsafe'):
			self.xsettag(*k)
			self.rawsave_keepcache()
		return x
	return w

def cachedsearch(length=20, submatch=True, casesensitive=False,
                 update=True):
	'''decorator constructor (wrap with a call: @cachedsearch())
	to cache expensive searches persistently using tagged class
	capabilities.

	How it works:
	- Wraps a search method expecting a string keyword to find
	- Perform the real search and store in cache the result
	  (found or not found)
	- Next time a keyword is similar or substring of a previous
	  search there is no need to perform the real search

	If there is a need to skip the cache invoke the wrapped method
	with cached=False.

	Cached data will be cleared when due (namely after .save),
	but there arent clever checks to detect invalid/obsolete values
	(not yet?).  Considering this, you should use this only if the
	computed value is supposed to never change.

	Arguments:
	  keyword: string to search
	  length: trim cache to this length
	  submatch: search substrings (not just whole keyword).
	            This functionality makes the algorithm search cache
	            to a greater extent, thus giving it higher probability
	            to hit.
	  update: reorder if keyword found (as in a FIFO, move keyword to the top)
	  casesensitive: It is supossed the declare the wrapped method's
	                 search sensitiveness. By default expect case insensitive
	                 searches.
	'''
	import operator
	from itertools import repeat

	def intercala(*lists):
		lists = list(lists)
		while lists:
			done = []
			for i,li in enumerate(lists):
				try:
					yield next(li)
				except StopIteration:
					done.insert(0, i)
			for d in done:
				lists.pop(d)

	def deco(func, neglength=(length*2)):
		def w(self, keyword, cached=True):
			key = [cachekey, 'search', func.__name__]
			cache = self.xgettag(*key) or {True:[], False:[]}
			herekeyword = keyword if casesensitive else keyword.lower()
			cmp = operator.contains if submatch else operator.is_

			if cached:
				# will travel both caches respecting its 'fifo position'
				# instead of forcing arbitrary precedence on one series...
				#
				tests1 = zip(cache[False], repeat(False),
				  repeat(lambda s:cmp(herekeyword, s)))

				tests2 = zip(cache[True], repeat(True),
				  repeat(lambda t:cmp(t, herekeyword)))

				for x, result, p in intercala(tests1, tests2):
					if p(x):
						self.microtrace('hit (cachedsearch)', result)
						if not hasattr(self, '_unsafe') and update:
							# this counter is debugging info
							count = key + [1]
							count[1] = 'searchhits'
							self.corexsettag(*count,
							             create=True, recreate=True,
							             overwrite=lambda n:n+1)
							# T o F
							cache[result].insert(0, cache[result].pop(x[0]))
							key.append(cache)
							self.xsettag(*key)
							self.rawsave_keepcache()
						return result

			found = bool(func(self, keyword))
			if not hasattr(self, '_unsafe') and update:
				# this counter is debugging info
				count = key + [1]
				count[1] = 'searchmisses'
				self.corexsettag(*count,
				             create=True, recreate=True,
				             overwrite=lambda n:n+1)

				cache[found].insert(0, herekeyword)
				cache[found] = cache[found][:length if found else neglength]
				key.append(cache)
				self.xsettag(*key)
				self.rawsave_keepcache()
			return found
		return w
	return deco


# XXX
# When operating above the mysql backend the BinaryFields get internally
# converted to 'str' when the documentation says it gets retrieved as 'bytes'.
# The retrieved data must be explicitly converted to bytes again
# at some specific points. To facilitate this conversion were gonna
# use pickling format 0 (ascii).
#
class tagged(abstractmodel):
	''' enrich a model to store dynamic data (usually data not
	predicted at development).

	The current implementation supports either one-level and n-level
	tag namespacing (methods starting with x)
	'''
	class Meta:
		abstract = True

	tags = models.BinaryField(null=True, editable=False)

	def cleantagcache(self):
		''' clears cache (tagcached) '''
		# .presave automatically deletes cache
		self.rawsave()

	def presave(self):
		# invalidate whole cache for this register
		# but obviously not when caching
		#
		try:
			delattr(self, cachekey)
		except AttributeError:
			self.xdeltag(cachekey)
		# use super's __setattr__ because we dont wanna
		# confuse 'modifiable' instances.
		super().__setattr__('tags', pickle.dumps(self.accesstags(), 0))
		# flag to not reparse .tags again
		self._tagged_save = None

	def rawsave_keepcache(self):
		# signal to not remove the tagcache
		setattr(self, cachekey, None)
		self.rawsave()

	@tdgetter
	def accesstags(self):
		''' return a copy of all tags as a dict '''
		if ISMYSQL and not hasattr(self, '_tagged_save'):
			# Reparse to produce 'bytes' data.
			# The repr of self.tags shows as b'...' but type(self.tags)
			# says it is 'str'. The .encode() method escapes the already
			# produced by escape sequences, breaking the pickling
			# format. The use of .eval over the expected bytes expression
			# produces the expected 'bytes' sequence
			#
			self.microtrace('tagged.accesstags:mysql blob conversion')
			tags = eval('b"' + self.tags[2:-1].replace('"', '\\"') + '"') \
						 if self.tags else None
		else:
			tags = self.tags
		return pickle.loads(tags) if tags else {}

	@tdsetter('accesstags', updatecache=True)
	def importtags(self, dicc):
		''' replace tags with the provided dictionary '''
		return dicc

	def hastag(self, tag):
		''' test if tag exists (one-level namespaced) '''
		return self.accesstags().__contains__(tag)

	def gettag(self, *a, **kw):
		''' get tag. Permits a default value if not found (one-level
		namespaced)
		'''
		return self.accesstags().get(*a, **kw)

	def settag(self, tag, val=None):
		''' set tag and its value (one-level namespaced) '''
		tags = self.accesstags()
		try:
			if val != tags[tag]:
				tags[tag] = val
				self.modified = True
		except KeyError:
			tags[tag] = val
			self.modified = True

	def xdeltag(self, *path):
		''' delete tag and its value (multi-level namespaced) '''
		path, key = path[:-1], path[-1]
		initd = self.accesstags()
		d = initd

		for p in path:
			d = d.get(p, None)
			if not isinstance(d, dict):
				return
		try:
			d.pop(key)
		except KeyError:
			pass
		else:
			self.modified = True


	def xhastag(self, *path):
		''' test if tag denoted by its *path* exists (n-level namespaced) '''
		d = self.accesstags()
		for p in path:
			try:
				d = d[p]
			except KeyError:
				return False
		return True

	def xgettag(self, *path):
		''' get tag denoted by its *path* (n-level namespaced).
		Permits a default value if not found.

		If the selected path exists but points to another
		namespacing level returns a dictionary with the contained
		tags
		'''
		d = self.accesstags()
		for p in path:
			try:
				d = d[p]
			except KeyError:
				return None
		return d

	def xsetprop(self, *args, **kw):
		''' set tag with no value (n-level namespaced).
		Also named property
		'''
		return self.xsettag(*(list(args) + [None]), **kw)

	def xsettag(self, *args):
		''' set tag denoted by its path and value (n-level namespaced)'''
		return self.corexsettag(
		  *args, overwrite=True, create=True, recreate=True)

	def corexsettag(self, *args, overwrite=False, create=False,
	                recreate=False, fail=True):
		args = list(args)
		val = args.pop()
		tag = args.pop()
		initd = self.accesstags()
		d = initd
		# vamos a recorrer el path y recrear diccionarios anidados si
		# es necesario (sobreescribiendo incluso)
		#
		for p in args:
			subd = d.get(p, None)
			if isinstance(subd, dict):
				d = subd
			elif recreate:
				d[p] = {}
				d = d[p]
			elif fail:
				raise TaggedException("tagged.corexsettag:not recreating nonexistent path")
			else:
				return
		isin = tag in d
		if (overwrite and isin):
			if callable(overwrite):
				val = overwrite(d[tag])
			if d[tag] != val:
				d[tag] = val
				self.modified = True
		elif (create and not isin):
			d[tag] = val
			self.modified = True
		elif fail:
			raise TaggedException("tagged.corexsettag:did not set value")


def show_tagcached(func):
	''' composable __str__ decorator showing cached methods in register '''
	def w(self):
		ks = self.gettag(cachekey).keys()
		if ks:
			s,ats = func(self)
			ats.append("tagcached(%s)" % ",".join(ks))
			return s,ats
		return func(self)
	return w

